/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package edu.webbapps.cormac_susilo.fit;

import com.garmin.fit.Decode;
import com.garmin.fit.FitRuntimeException;
import com.garmin.fit.MesgBroadcaster;
import com.garmin.fit.RecordMesg;
import com.garmin.fit.RecordMesgListener;
import com.garmin.fit.SessionMesg;
import com.garmin.fit.SessionMesgListener;
import edu.webbapps.cormac_susilo.bootstrap.ConversionException;
import edu.webbapps.cormac_susilo.core.Frame;
import edu.webbapps.cormac_susilo.core.Run;
import edu.webbapps.cormac_susilo.core.User;
import java.io.InputStream;
import java.io.IOException;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Logger;

/**
 * Class with utility methods and application logic to covert a .fit file to
 * objects that better suit our needs. Maps the messages stored in the fit file
 * to our data model.
 *
 * When a file is uploaded it can be converted and inserted into existing data
 * objects, which are provided
 *
 * @author tor
 */
public class CormacFitDecoder {

    /**
     * Attempt to load data into the User object from the file.
     * No data is loaded into the user object if an error occurs during the process.
     * 
     * @param user a persisted user object
     * @param in in input stream of a valid .fit file
     * @param trackname the track name to be assigned to the resulting Run
     * @param dropframes specifies if to silently drop frames or throw exception
     * @throws ConversionException if a file cannot be completely converted, 
     * if dropframes is TRUE it will drop occasional bad frames (expected).
     * A ConversionException is always thrown if a file cannot be parsed.
     */
    public static void loadDataIntoUser(final User user, InputStream in, String trackname, boolean dropframes) throws ConversionException {

        Run run = new Run(trackname);
        try {

            List<Frame> frames = convertFile(in, run, dropframes);
            run.setFrames(frames);
            List<Run> runs = user.getRuns();
            runs.add(run);
            run.setUser(user);
            user.setRuns(runs);
            user.setTotal_distance(user.getTotal_distance() + run.getDistance());
            user.setTotal_time(user.getTotal_time() + run.getTotalTime());

        } catch (ConversionException e) {
            throw e;
        } catch (IOException e) {
            Logger.getAnonymousLogger().severe(e.getMessage());
            e.printStackTrace();
        } catch ( NullPointerException e) {
            throw new IllegalArgumentException("Got nullpointer, is User persisted?");
        }
    }

    /**
     * Convert an input stream (.fit file) and insert into provided objects.
     *
     */
    private static List<Frame> convertFile(InputStream in, final Run run, final boolean dropframes) throws ConversionException, IOException {
        if (in == null) {
            throw new IllegalArgumentException("Provided argument was null");
        }
        final List<Frame> frames = new LinkedList<Frame>();

        if (CormacFitDecoder.performTests(in)) {

            final Decode decode = new Decode();
            final MesgBroadcaster mesgBroadcaster = new MesgBroadcaster(decode);


            mesgBroadcaster.addListener(new SessionMesgListener() {
                @Override
                public void onMesg(SessionMesg mesg) {
                    try {
                        Run r = CormacFitDecoder.convertRun(mesg);

                        /*
                         * Session ony contains some of the information
                         * we want for Run, the other attributes must be set
                         * by other means.
                         */
                        run.setDistance(r.getDistance());
                        run.setRun_date(r.getRun_date());
                        run.setTotalTime(r.getTotalTime());
                        run.setAvg_heartrate(r.getAvg_heartrate());
                        run.setMax_heartrate(r.getMax_heartrate());
                        run.setAvg_speed(r.getAvg_speed());
                        run.setMax_speed(r.getMax_speed());
                        run.setTotal_calories(r.getTotal_calories());
                    } catch (ConversionException e) {
                        if (!dropframes) {
                            throw e;
                        }
                    }
                }
            });

            mesgBroadcaster.addListener(new RecordMesgListener() {
                @Override
                public void onMesg(RecordMesg mesg) {
                    try {
                        Frame f = CormacFitDecoder.convertFrame(mesg);
                        f.setRun(run);
                        frames.add(f);
                    } catch (ConversionException e) {
                        if (!dropframes) {
                            throw e;
                        }
                    }
                }
            });

            try {
                mesgBroadcaster.run(in);

            } catch (FitRuntimeException e) {
                throw new ConversionException(e.getMessage());
            }

            try {
                in.close();
            } catch (IOException e) {
                throw e;
            }

        } else {
            throw new ConversionException("FileInputStream failed integrity check");
        }

        return frames;
    }

    /**
     * Garmin devices stores the degree as an integer and it has to be converted
     * to make sense to maps.
     *
     * @param semicircles
     * @return the degree represented by the semicircle
     */
    private static double toDegrees(int semicircles) {
        return semicircles * (180.0 / Math.pow(2.0, 31.0));
    }

    private static Run convertRun(SessionMesg mesg) throws ConversionException {

        Run r = new Run();
        try {
            r.setRun_date(mesg.getStartTime().getDate());
            r.setDistance(mesg.getTotalDistance());
            r.setTotalTime(mesg.getTotalElapsedTime());            
            r.setAvg_heartrate(mesg.getAvgHeartRate()); //short
            r.setMax_heartrate(mesg.getMaxHeartRate()); //short    
            r.setAvg_speed(mesg.getAvgSpeed()); //f
            r.setMax_speed(mesg.getMaxSpeed()); //f           
            r.setTotal_calories(mesg.getTotalCalories()); //int
            
        } catch (Exception e) {
            throw new ConversionException("Error converting Session " + e.getMessage());
        }
        return r;
    }

    /**
     * Convert a RecordMesg to Frame object
     *
     * @param mesg the RecordMesg generated by the fit file
     * @return a converted Frame
     */
    private static Frame convertFrame(RecordMesg mesg) throws ConversionException {
        Frame f = new Frame();
        try {
            Date dt = mesg.getTimestamp().getDate();
            f.setTime_label(dt);
            float speed = mesg.getSpeed();
            f.setSpeed(speed); //m/s
            short heartrate = mesg.getHeartRate();
            f.setPulse(heartrate);
            int lon = mesg.getPositionLong();
            f.setLon(CormacFitDecoder.toDegrees(lon));
            int lat = mesg.getPositionLat();
            f.setLat(CormacFitDecoder.toDegrees(lat));
            float alt = mesg.getAltitude();
            f.setAlt(alt);
            float distance = mesg.getDistance();
            f.setDistance(distance);
        } catch (Exception e) {
            throw new ConversionException("Error converting Record " + e.getMessage());
        }
        return f;
    }

    private static boolean performTests(InputStream in) {
        return true; //Decode.checkIntegrity(in); needs to restart stream etc...
    }
}
